use windows::core::{GUID, Result as WinResult, PCWSTR};
use windows::Win32::System::Com::{
    CoInitializeEx,
    CoUninitialize,
    COINIT_APARTMENTTHREADED,
    IDispatch,
    CLSIDFromProgID,
    CoCreateInstance,
    CLSCTX_ALL,
    EXCEPINFO,
    DISPPARAMS,
    DISPATCH_FLAGS,
    DISPATCH_METHOD,
    DISPATCH_PROPERTYPUT,
    DISPATCH_PROPERTYGET,
};
use windows::Win32::System::Ole::DISPID_PROPERTYPUT;
use windows::core::VARIANT;

use crate::variant::{variant_empty};

pub struct ComApartment;
impl ComApartment {
    pub fn init_sta() -> WinResult<Self> {
        unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED).ok()?; }
        Ok(Self)
    }
}
impl Drop for ComApartment {
    fn drop(&mut self) {
        unsafe { CoUninitialize(); }
    }
}

fn to_wide_null(s: &str) -> Vec<u16> {
    s.encode_utf16().chain(std::iter::once(0)).collect()
}

/// 检测指定 ProgID 是否在系统注册表中有效（已注册）。
/// 返回 Ok(true) 表示可用；Ok(false) 表示未注册；Err 表示调用过程中发生错误。
pub fn is_progid_registered(progid: &str) -> WinResult<bool> {
    // 尝试通过 CLSIDFromProgID 解析，如果失败则认为未注册。
    let wide = to_wide_null(progid);
    unsafe {
        match CLSIDFromProgID(PCWSTR(wide.as_ptr())) {
            Ok(_) => Ok(true),
            Err(err) => {
                // 对于 0x800401F3 (invalid class string) 等错误，返回 Ok(false)
                let code = err.code().0;
                if code as u32 == 0x800401F3u32 {
                    Ok(false)
                } else {
                    Err(err)
                }
            }
        }
    }
}

pub fn get_dispid(dispatch: &IDispatch, name: &str) -> WinResult<i32> {
    let mut dispid = 0i32;
    let wide = to_wide_null(name);
    let names = [PCWSTR(wide.as_ptr())];
    let riid = GUID::zeroed();
    unsafe {
        dispatch.GetIDsOfNames(
            &riid,
            names.as_ptr(),
            1,
            0,
            &mut dispid,
        )?;
    }
    Ok(dispid)
}

pub fn invoke(
    dispatch: &IDispatch,
    name: &str,
    flags: DISPATCH_FLAGS,
    args: &mut [VARIANT],
    named_ids: Option<&mut [i32]>,
) -> WinResult<VARIANT> {
    args.reverse(); // COM 需要逆序

    let dispid = get_dispid(dispatch, name)?;
    let mut excep = EXCEPINFO::default();
    let (named_ptr, named_len) = {
        let mut ptr = std::ptr::null_mut();
        let mut len = 0u32;
        if let Some(ids) = named_ids {
            if !ids.is_empty() {
                ptr = ids.as_mut_ptr();
                len = ids.len() as u32;
            }
        }
        (ptr, len)
    };
    let mut arg_dp = DISPPARAMS {
        rgvarg: if args.is_empty() { std::ptr::null_mut() } else { args.as_mut_ptr() },
        rgdispidNamedArgs: named_ptr,
        cArgs: args.len() as u32,
        cNamedArgs: named_len,
    };

    let mut result = variant_empty();
    unsafe {
        dispatch.Invoke(
            dispid,
            &GUID::zeroed(),
            0,
            flags,
            &mut arg_dp,
            Some(&mut result as *mut VARIANT),
            Some(&mut excep as *mut EXCEPINFO),
            Some(std::ptr::null_mut()),
        )?;
    }
    Ok(result)
}

#[derive(Clone)]
pub struct ComObject {
    pub disp: IDispatch,
}
impl ComObject {
    pub fn new(disp: IDispatch) -> Self { Self { disp } }

    pub fn get(&self, prop: &str) -> WinResult<VARIANT> {
        let mut args: [VARIANT; 0] = [];
        invoke(&self.disp, prop, DISPATCH_PROPERTYGET, &mut args, None)
    }
    pub fn put(&self, prop: &str, value: VARIANT) -> WinResult<()> {
        let mut args = [value];
        let mut named = [DISPID_PROPERTYPUT];
        let _ = invoke(&self.disp, prop, DISPATCH_PROPERTYPUT, &mut args, Some(&mut named))?;
        Ok(())
    }
    pub fn call(&self, method: &str, mut args: Vec<VARIANT>) -> WinResult<VARIANT> {
        invoke(&self.disp, method, DISPATCH_METHOD, &mut args[..], None)
    }
}

pub fn com_create(progid: &str) -> WinResult<ComObject> {
    let wide = to_wide_null(progid);
    let clsid = unsafe { CLSIDFromProgID(PCWSTR(wide.as_ptr()))? };
    let disp: IDispatch = unsafe { CoCreateInstance(&clsid, None, CLSCTX_ALL)? };
    Ok(ComObject::new(disp))
}